package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * @author 21829
 */
@Component
@Slf4j
public class CacheClient {

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public final StringRedisTemplate stringRedisTemplate;


    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set(String key, Object value, Long expire, TimeUnit timeUnit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),expire,timeUnit);
    }

    public void setLogicalExpire(String key, Object value, Long expire, TimeUnit timeUnit){
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(expire)));
        redisData.setData(value);
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }

    public  <ID,R> R queryWithPassThrough(String keyPrefix, Class<R> type, Function<ID,R> dbFallback, ID id, Long time, TimeUnit timeUnit) {
        //1.从redis中查询商户缓存
        String json = stringRedisTemplate.opsForValue().get(keyPrefix + id);
        //2.判断是否为空，不为空为命中转换为对应的商户对象返回
        //三种情况 真实命中缓存在redis中商户信息  命中缓存中为“”的商户数据 没有命中则为null去查询数据库
        if (StrUtil.isNotBlank(json)) {
            return JSONUtil.toBean(json, type);
        }
        //2.1判断是否真正命中缓存
        if (json != null){
            return null;
        }
        //3.为空则代表未命中 根据商户id查询数据库
        R byIdJson = dbFallback.apply(id);
        //3.1 判断从数据库中查询出来的商户是否为空 为空则写入redis
        if (byIdJson == null) {
            stringRedisTemplate.opsForValue().set(keyPrefix + id,"",time, timeUnit);
            return null;
        }
        //4.存在写入redis并返回
        stringRedisTemplate.opsForValue().set(keyPrefix + id, JSONUtil.toJsonStr(byIdJson),time, timeUnit);
        return byIdJson;
    }

    public  <ID,R> R queryWithLogicalExpired(String keyPrefix,ID id,Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit timeUnit) {
        String key = keyPrefix+ id;
        //1.从redis中查询商户缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断缓存是否命中 未命中返回空
        if (StrUtil.isBlank(json)) {
            return null;
        }
        //3.命中
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R bean = JSONUtil.toBean(JSONUtil.toJsonStr(redisData.getData()), type);
        LocalDateTime expireTime = redisData.getExpireTime();
        //4.判断缓存商户信息是否过期  如果为true则表示该商户信息未过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            return bean;
        }
        //5.尝试获取互斥锁 判断是否拿到互斥锁
        try {
            boolean tryLock = tryLock(RedisConstants.LOCK_SHOP_KEY+id);
            if (tryLock){
                String dataStr = stringRedisTemplate.opsForValue().get(keyPrefix + id);
                RedisData dataJson = JSONUtil.toBean(dataStr, RedisData.class);
                if (dataJson.getExpireTime().isAfter(LocalDateTime.now())) {
                    return JSONUtil.toBean(JSONUtil.toJsonStr(dataJson.getData()),type);
                }
                CACHE_REBUILD_EXECUTOR.submit(()->{
                    //1.先查询数据库
                    R apply = dbFallback.apply(id);
                    this.setLogicalExpire(key, apply, time, timeUnit);
                });
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //释放锁
            unLock(RedisConstants.LOCK_SHOP_KEY+id);
        }
        return bean;
    }

    //加锁
    private boolean tryLock(String key) {
        Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(ifAbsent);
    }
    //释放锁
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }
}
